﻿module VirtualCity.Scripts.PlanExecutor

open Casanova
open Casanova.Utilities
open Casanova.Core
open Casanova.Coroutines
open Casanova.Game
open Casanova.Input
open Casanova.Math
open Casanova.Drawing
open VirtualCity
open VirtualCity.City
open VirtualCity.Plan
open Wintellect.PowerCollections


let inline (!) x = immediate_lookup x


/// The walk action simply takes the agent to a given point.
let walk validate_walk_graph (target:Vector2<m>) (city:City) (agent:Agent) = 
  co{
    if not validate_walk_graph ||
       city.WalkGraph.Links |> Seq.exists (fun (n1,n2) -> Vector2.Distance(n1.Position, !agent.Physical.Position) <= 1.0f<m> &&
                                                          Vector2.Distance(n2.Position, target) <= 1.0f<m>) then
      do agent.Physical.Target   := target
      do agent.Physical.MaxSpeed := 5.0f<km/h> |> to_meters_per_second
      do! wait 60.0f<s>
      do! wait_condition (fun () -> (!agent.Physical.Velocity).Length <= 0.01f<m/s>)
      do! wait_condition (fun () -> Vector2.Distance(target, !agent.Physical.Position) <= 0.01f<m>)
      do agent.Physical.Position := target
      return true
    else
      do agent.CurrentAction := sprintf "Failed walking:\nposition is not part of the\nwalkable paths"
      return false
  }


/// Boarding a bus requires that the agent pays the price for the ticket.
/// After that, the agent is simply set in the list of people waiting for the bus
/// at the local stop, from which he will board the bus. The bus will then leave
/// the agent at the destination.
let board (bus:Bus) (source:BusStop) (destination:BusStop) (city:City) (agent:Agent) =
  co{
//    if Vector2<m>.Distance(source.Location,!agent.Physical.Position) <= 3.0f<m> &&
//        bus.TicketPrice <= !agent.Money && source.Time > !city.Clock.Time then
//      do agent.Money := !agent.Money - bus.TicketPrice
//      do source.BusWaiters := 
//          {
//            Agent       = ref agent
//            BusNumber   = bus.Number
//            Destination = ref destination
//          } :: !destination.BusWaiters
//      do! wait_condition (fun () -> Vector2<m>.Distance(destination.Location,!agent.Physical.Position) <= 3.0f<m>)
//      return true
//    else
      do agent.CurrentAction := "Failed boarding bus"
      return false
  }


let contains_all (items:List<string>) (inventory:List<Item>) =
  let rec remove_first ((l:List<Item>),found) y =
    match l with
    | x :: xs ->
      if x.Name = y then
        xs,found
      else
        let xs,found = remove_first (xs,found) y
        x::xs,found
    | [] -> [],false
  List.fold remove_first (inventory,true) items |> snd


let remove_all (items:List<string>) (inventory:List<Item>) =
  let rec remove_first (l:List<Item>) y =
    match l with
    | x :: xs ->
      if x.Name = y then
        xs
      else
        x :: remove_first xs y
    | [] -> []
  List.fold remove_first inventory items


/// Using an item takes a certain amount of time; after that time,
/// the user receives some items, money, and stats, but he also
/// loses some others. For example, the user may obtain an item,
/// but lose money when going shopping.
let use_item (item:Item) (use_effect_index:int) (city:City) (agent:Agent) = 
  co{
    let clock = city.Clock
    let use_effects = item.UseEffects.[use_effect_index]
    let use_duration = use_effects.TimeSpent
    let time_slot_ok = item.UseSlots |> Seq.exists (UseSlot.Contains (!clock.Time))
    let stats_ok = Stats.LessEqual(use_effects.StatsLost,!agent.Stats)
    let items_ok = contains_all use_effects.ItemsLost agent.Inventory
    let money_ok = use_effects.MoneyLost <= !agent.Money
    let location_ok = Vector2.Distance(item.Location, !agent.Physical.Position) < 2.0f<m>
    let open_use_ok = 
      item.Base.OpenUse ||
      Vector2.Distance(item.Location, (!agent.Home).Position) <= 1.0f<m> ||
      Vector2.Distance(item.Location, (!agent.Work).Position) <= 1.0f<m>
    if time_slot_ok && stats_ok && items_ok && money_ok && location_ok && open_use_ok then
      let use_effects = use_effects
      agent.Stats := !agent.Stats + use_effects.StatsGained
      agent.Stats := !agent.Stats - use_effects.StatsLost * (!agent.Stats).StressHandicap
      let home = !agent.Home
      let lost_items = use_effects.ItemsLost
      let new_items = 
        [ for item in use_effects.ItemsGained do
            let item = !item
            yield
              { 
                Item.Visual     = None
                Physical        = None
                Base            = item
                Location        = (!agent.Home).Base.Position
              }
        ]
      home.Items :=
        [
          yield! new_items
          yield! remove_all lost_items !home.Items
        ]
      agent.Money := !agent.Money + use_effects.MoneyGained
      agent.Money := !agent.Money - use_effects.MoneyLost
      agent.StatsIdleMask := Stats.Zero
      do! wait use_duration
      agent.StatsIdleMask := Stats.One
      return true
    else
      let failure_reason = 
        [time_slot_ok,"time slot"
         stats_ok,"stats"
         items_ok,"items" 
         money_ok,"money" 
         location_ok,"location"] 
          |> Seq.find (fst >> not)
          |> snd
      do agent.CurrentAction := sprintf "Failed using item\n %s \n because of %s" item.Name failure_reason
      return false
  }


/// Taking a step in a plan checks the current action
/// to perform and performs it. In the meantime, we also
/// check the expectations for this step: if they are not 
/// met, this means that the plan should be aborted and a
/// new planning stage should be launched
let take_step validate_walk_graph (city:City) (agent:Agent) (step:Step) =
  co{
    match step.Action with
    | Walk target -> 
      agent.CurrentAction := "Walk"
      return! agent |> walk validate_walk_graph target city
    | Board bus -> 
      agent.CurrentAction := "Bus"
      let bus,source,destination = !bus
      return! agent |> board bus source destination city
    | Use(item,use_effect_index) -> 
      let item = !item
      agent.CurrentAction := item.UseEffects.[use_effect_index].Description
      return! agent |> use_item item use_effect_index city
  }


/// A plan description stores the actions to perform and the
/// expectations that describe what was planned to happen with 
/// the execution of the various actions. The plan description
/// does not refer to the actual objects in the game world, but
/// rather to their description.
/// A plan description can be transformed into a concrete, 
/// executable plan by converting the descripted actions into
/// concrete actions on the equivalent items of the virtual
/// city.
let plan_description_to_plan (city:City) (plan:PlanDescription.PlanDescription<PlanDescription.ActionDescription>) = 
  {
    Plan.Steps = 
      [
        for step in plan.Steps do
          let action =           
            match step.Action with
            | PlanDescription.ActionDescription.Walk target -> 
              Walk(target)
//            | PlanDescription.ActionDescription.Board (bus,source,destination) -> 
//              let bus = city.Buses |> Seq.find (fun b -> b.Number = bus.Number)
//              let source = bus.Stops |> Seq.find (fun s -> s.Name = source.Name)
//              let destination = bus.Stops |> Seq.find (fun s -> s.Name = destination.Name)
//              Board(ref(bus,source,destination))
            | PlanDescription.ActionDescription.Use(location,item,use_effect_index) ->
              let items = seq{ for b in city.Buildings do yield! !b.Items } |> Seq.filter (fun i -> i.Name = item.Name && Vector2.Distance(i.Location,location) <= 2.0f<m>)
              if items |> Seq.length = 0 then 
                failwithf "There are no accessible items named %s at the specified location" item.Name
              else
                Use(items |> Seq.head |> ref,use_effect_index)
          yield {
                  Action       = action
                  Expectations = step.Expectations
                }
      ]
  }


/// To follow a plan, first we convert it into concrete form,
/// and then we take all of its steps.
let follow_plan check_expectations validate_walk_graph (city:City) (agent:Agent) =
  let initial_stats = !agent.Stats
  let plan = !agent.ActivePlan |> plan_description_to_plan city
  let rec take_steps steps = 
    co{
      do! yield_
      match steps with
      | step::steps ->  
        let! res = take_step validate_walk_graph city agent step
        let time_diff = !city.Clock.Time - step.Expectations.City.Clock.Time
        let actual_stats = !agent.Stats
        let expected_stats = step.Expectations.Agent.Stats 
        let time_ok = time_diff < TimeSpan.FromMinutes(20.0)
        let stats_ok = actual_stats.VitalMin >= expected_stats.VitalMin
        let agent_alive = actual_stats.VitalMin > 0.0f

//        if not agent_alive then
//          printf "Agent %s died at %s.\nAction = %s\nExpectations = (%s,%s,%s)\nReality = (%s,%s,%s)\n"
//                  agent.Name
//                  ((!city.Clock.Time).ToString())
//                  (step.Action.ToString()) 
//                  (step.Expectations.Agent.Position.ToXNA.ToString())
//                  (step.Expectations.Agent.Stats.ToString())
//                  (step.Expectations.City.Clock.Time.ToString())
//                  ((!agent.Physical.Position).ToXNA.ToString())
//                  ((!agent.Stats).ToString())
//                  ((!city.Clock.Time).ToString())
//          do! wait 600.0f<s>
          
//        if not stats_ok || not time_ok then
//          agent.DebugLog := 
//            sprintf "%s\nAction = %s\nExpectations = (%s,%s,%s)\nReality = (%s,%s,%s)\n"
//                    !agent.DebugLog 
//                    (step.Action.ToString()) 
//                    (step.Expectations.Agent.Position.ToXNA.ToString())
//                    (step.Expectations.Agent.Stats.ToString())
//                    (step.Expectations.City.Clock.Time.ToString())
//                    ((!agent.Physical.Position).ToXNA.ToString())
//                    ((!agent.Stats).ToString())
//                    ((!city.Clock.Time).ToString())

        if res && (not check_expectations || (stats_ok && time_ok)) && agent_alive then
          return! take_steps steps
        else
          if !agent.Selected then
            let failure_reason = 
              [res,!agent.CurrentAction
               stats_ok,"stats are lower than expected"
               time_ok,"time is not what was expected" 
               agent_alive,"agent is not alive"] 
                |> Seq.find (fst >> not)
                |> snd
            do printf "Failed plan because \n%s\n" failure_reason
          return false
      | [] -> 
        agent.CurrentAction := "Finished planning"
        return true
    }
  take_steps plan.Steps
